package me.ratel.demo.wechat;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.net.UrlEscapers;
import com.mashape.unirest.http.HttpResponse;
import com.mashape.unirest.http.Unirest;
import com.mashape.unirest.http.exceptions.UnirestException;
import me.ratel.demo.eBook.infrastructure.exceptions.BusinessException;
import me.ratel.demo.wechat.entities.BrandWCPayRequest;
import me.ratel.demo.eBook.interfaces.web.WebError;
import me.ratel.demo.wechat.config.Configuration;
import me.ratel.demo.wechat.config.ConfigurationImpl;
import me.ratel.demo.wechat.entities.*;
import me.ratel.demo.wechat.exceptions.InvalidCredentialException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;

/**
 * @author Administrator
 * @time 2016/9/15 16:20
 */
class WeChatServiceImpl implements WeChatService {
    private static final Logger log = LoggerFactory.getLogger(WeChatServiceImpl.class);

    // 公众号Id/密码
    protected String appId;
    protected String appSecret;

    // 商户Id/key
    protected String mchId;
    protected String apiKey;

    protected final AccessToken accessToken;

    protected final Configuration configuration;

    private static final Cache<String, WeChatServiceImpl> cache = CacheBuilder.newBuilder()
            .expireAfterAccess(1, TimeUnit.SECONDS)
            .maximumSize(1000).build();

    private WeChatServiceImpl() {
        this(null, null);
    }

    private WeChatServiceImpl(String appId, String appSecret) {
        this(appId, appSecret, null, null);
    }

    private WeChatServiceImpl(String appId, String appSecret, String mchId, String apiKey) {
        setAppId(appId);
        setAppSecret(appSecret);

        setMchId(mchId);
        setApiKey(apiKey);

        this.accessToken = new AccessToken();
        this.configuration = ConfigurationImpl.getConfiguration();
    }

    public static WeChatService newWeChat() {
        return newWeChat(null, null);
    }

    public static WeChatService newWeChat(String appId, String appSecret) {
        return newWeChat(appId, appSecret, null, null);
    }

    public static WeChatService newWeChat(String appId, String appSecret, String mchId, String apiKey) {
        try {
            return cache.get(appId, () -> new WeChatServiceImpl(appId, appSecret, mchId, apiKey));
        } catch (ExecutionException ee) {
            log.error("get wechat instance error", ee);
        }
        return null;
    }

    public String getAppId() {
        return appId;
    }

    public void setAppId(String appId) {
        this.appId = appId;
    }

    public String getAppSecret() {
        return appSecret;
    }

    public void setAppSecret(String appSecret) {
        this.appSecret = appSecret;
    }

    public String getMchId() {
        return mchId;
    }

    public void setMchId(String mchId) {
        this.mchId = mchId;
    }

    public String getApiKey() {
        return apiKey;
    }

    public void setApiKey(String apiKey) {
        this.apiKey = apiKey;
    }

    class AccessToken {
        private String accessToken;

        public String getAccessToken() {
            return accessToken;
        }

        public void setAccessToken(String accessToken) {
            this.accessToken = accessToken;
        }

        public String refreshAccessToken() {
            synchronized (WeChatServiceImpl.this) {
                if (testAvailable()) return getAccessToken();
                try {
                    JSONObject response = JSON.parseObject(
                            Unirest.post(configuration.getTokenURL())
                                    .field("grant_type", "client_credential")
                                    .field("appId", getAppId())
                                    .field("secret", getAppSecret()).asString().getBody()
                    );
                    if (response.containsKey("access_token")) setAccessToken(response.getString("access_token"));
                } catch (UnirestException ue) {
                    throw new BusinessException(ue, new WebError(
                            WeChatConstants.ErrorHeader.REQUEST_WECHAT_SERVER_ERROR, null
                    ));
                }
            }
            return getAccessToken();
        }

        private boolean testAvailable() {
            try {
                return JSON.parseObject(
                        Unirest.post(configuration.getMenuURL())
                                .field("access_token", getAccessToken())
                                .asString().getBody(), GetMenuResponse.class
                ).getMenu() != null;
            } catch (UnirestException ue) {
                throw new BusinessException(ue, new WebError(
                        WeChatConstants.ErrorHeader.REQUEST_WECHAT_SERVER_ERROR, null
                ));
            }
        }
    }

    @Override public GetMenuResponse getMenu() {
        // 调用查询接口
        String body = null;
        try {
            body = Unirest.post(configuration.getMenuURL())
                    .field("access_token", accessToken.getAccessToken())
                    .asString().getBody();
        } catch (UnirestException ue) {
            throw new BusinessException(ue, new WebError(
                    WeChatConstants.ErrorHeader.REQUEST_WECHAT_SERVER_ERROR, null
            ));
        } finally {
            log.info(body, new Object[]{});
        }

        // 判断返回值
        GetMenuResponse getMenuResponse = JSON.parseObject(body, GetMenuResponse.class);
        if (getMenuResponse.getMenu() != null) return getMenuResponse;
        else if (!ResponseCode.INVALID_CREDENTIAL
                .sameWithErrCode(JSON.parseObject(body).getInteger("errcode"))) return null;
        else {
            accessToken.refreshAccessToken();
            throw new InvalidCredentialException();
        }
    }

    @Override public UnifiedOrderResponse unifiedOrder(String body, JSONObject detail, String notifyURL, String outTradeNo, String openId, String spbillCreateIP, int totalFee, String tradeType) {
        try {
            UnifiedOrderRequest request = UnifiedOrderRequestBuilder.newBuilder()
                    .setAppIdFluent(getAppId())
                    .setMchIdFluent(getMchId())
                    .setBodyFluent(body)
                    .setDetailFluent(detail.toJSONString())
                    .setNotifyUrlFluent(notifyURL)
                    .setOpenidFluent(openId)
                    .setOutTradeNoFluent(outTradeNo)
                    .setSpbillCreateIPFluent(spbillCreateIP)
                    .setTotalFeeFluent(totalFee)
                    .setTradeTypeFluent(tradeType)
                    .setApiKeyFluent(getApiKey()).build();

            HttpResponse<String> response = Unirest.post(configuration.getUnifiedOrderURL())
                    .header("Content-Type", "text/xml")
                    .body(request.toXML()).asString();

            return UnifiedOrderResponse.toBean(response.getRawBody());
        } catch (UnirestException ue) {
            throw new BusinessException(ue, new WebError(
                    WeChatConstants.ErrorHeader.REQUEST_WECHAT_SERVER_ERROR, null
            ));
        }
    }

    @Override public BrandWCPayRequest getBrandWCPayRequest(String prepayId) {
        return new BrandWCPayRequest(appId, prepayId, apiKey);
    }

    @Override public OrderQueryResponse orderQuery(String outTradeNo) {
        try {
            OrderQueryRequest request = OrderQueryRequestBuilder.newBuilder()
                    .setAppIdFluent(getAppId())
                    .setMchIdFluent(getMchId())
                    .setOutTradeNoFluent(outTradeNo).build();

            HttpResponse<String> response = Unirest.post(configuration.getOrderQueryURL())
                    .header("Content-Type", "application/xml")
                    .body(request.toXML()).asString();

            return OrderQueryResponse.toBean(response.getRawBody());
        } catch (UnirestException e) {
            throw new BusinessException(e, new WebError(
                    WeChatConstants.ErrorHeader.REQUEST_WECHAT_SERVER_ERROR, null
            ));
        }
    }

    @Override public String createOauth2AuthorizeURL(String redirectUri) {
        return configuration.getOauth2AuthorizeURL()
                + "?appid=" + getAppId()
                + "&redirect_uri=" + UrlEscapers.urlFormParameterEscaper().escape(redirectUri)
                + "&response_type=" + "code"
                + "&state=" + "state"
                + "#wechat_redirect";
    }

    @Override public SnsOAuth2AccessTokenResponse snsOAuth2AccessToken(String code) {
        try {
            return JSON.parseObject(
                    Unirest.post(configuration.getSnsOAuth2AccessTokenURL())
                            .field("appid", getAppId())
                            .field("secret", getAppSecret())
                            .field("code", code)
                            .field("grant_type", "authorization_code")
                            .asString().getBody(),
                    SnsOAuth2AccessTokenResponse.class
            );
        } catch (UnirestException e) {
            throw new BusinessException(e, new WebError(
                    WeChatConstants.ErrorHeader.REQUEST_WECHAT_SERVER_ERROR, null
            ));
        }
    }

    @Override public SnsOAuth2AccessTokenResponse refreshOAuth2AccessToken(String refreshToken) {
        try {
            return JSON.parseObject(
                    Unirest.post(configuration.getSnsOAuth2AccessTokenURL())
                            .field("appid", getAppId())
                            .field("grant_type", "refresh_token")
                            .field("refresh_token", refreshToken).asString().getBody(),
                    SnsOAuth2AccessTokenResponse.class
            );
        } catch (UnirestException e) {
            throw new BusinessException(e, new WebError(
                    WeChatConstants.ErrorHeader.REQUEST_WECHAT_SERVER_ERROR, null
            ));
        }
    }

    @Override public OAuth2UserInfo snsUserInfo(String accessToken, String openId) {
        try {
            return JSON.parseObject(
                    Unirest.post(configuration.getSnsOAuth2AccessTokenURL())
                            .field("access_token", accessToken)
                            .field("openid", openId)
                            .field("lang", "zh_CN").asString().getBody(),
                    OAuth2UserInfo.class
            );
        } catch (UnirestException e) {
            throw new BusinessException(e, new WebError(
                    WeChatConstants.ErrorHeader.REQUEST_WECHAT_SERVER_ERROR, null
            ));
        }
    }
}
